To manage the complexity of long computer programs, computer programmers often adopt object-oriented programming techniques. With these techniques, a computer program is organized as multiple smaller modules called objects. These objects perform specified functions and interact with other objects in pre-defined ways. FIG. 1 shows several principles of object-oriented programming with reference to an object 100 that interacts in pre-defined ways with a client 140 (which can also be an object).
The object 100 encapsulates data 110, which represents the current state of the object 100. The data 110 can be organized as data elements such as properties of the object 100. The object 100 exposes member functions (alternatively called methods) 120, 122, and 124 that provide access to the data 110 or provide some other defined function or service. To access the data 110, the client 140 of the object 100 goes through a member function. In FIG. 1, the member functions 120, 122, 124 are grouped into an interface 126. FIG. 1 also includes an interface 130 (shown without member functions for simplicity).
The object 100 and the client 140 interact across the interfaces exposed by the object 100. For example, for the client 140 to invoke the member function 120, a function call in the client 140 identifies the member function 120, provides any data to be processed (e.g., input parameters for arguments), and indicates any data to be returned (e.g., output parameters for arguments, return values). Similarly, for the client 140 to set a value for a property of the data 110, the client 140 identifies the property and provides the value.
The client 140 initially identifies the member functions of the object 100 by name. Before the client 140 can access a member function of the object 100, the client's name for the member function must become associated with the actual member function in the object 100 in a binding process. Binding can occur at any of several different times, and the best time for binding depends on the situation.
When the client 140 and the object 100 are tightly integrated, it is often efficient to use early binding. With early binding, the names in the client 140 are associated with the actual respective member functions of the object 100. The client 140 directly operates upon the member functions of the object 100 through an interface.
FIG. 2 shows an object 200 that provides an early binding implementation of the interface ITest. The object 200 is designed according to Microsoft Corporation's Component Object Model [“COM”] and includes an instance data structure 202, a virtual function table 210 for ITest, and method implementations 221–226. The instance data structure 202 includes a pointer 204 to the virtual function table 210 and data 208, such as properties. In early binding, a client is compiled to use the virtual function table and function signatures of ITest. A client references the early binding interface using a pointer to the pointer 204. The virtual function table 210 includes a pointer for each member function of ITest. The pointers 211–213 correspond to the standard COM IUnknown methods QueryInterface, AddReference, and Release, and point to implementations 221–223, respectively. The pointers 214–216 correspond to the other methods of ITest, and point to implementations 224–226, respectively. For additional information about COM and early binding interfaces, see Kraig Brockschmidt, Inside OLE, second edition, Microsoft Press (1995).
While early binding works well for a tightly integrated object and client, an object might need to work with a client that cannot use a custom early binding mechanism for the object. In such situations, late binding works well. With late binding, an object exposes an intermediary interface. A client accesses late bound member functions of an object through this intermediary interface.
Late binding can be provided through numerous mechanisms. One mechanism uses tokens to identify the late bound member functions of an object. A client associates its member function names and property names with corresponding tokens. To invoke a late bound member function, the client passes the corresponding token to the intermediary interface, along with any arguments for the member function (typically packed into a generic data structure). The intermediary interface then calls the member function identified by the token. With late binding, names can be associated with tokens at run time or at compile time. If the association occurs at compile time, the tokens are still used to invoke late bound methods through the intermediary interface at run time, but a client can call the intermediary interface without first looking up a token for a name at run time.
FIG. 3 shows one type of intermediary interface, a dispatch interface implemented by a COM object 300. In FIG. 3, the dispatch interface provides a late binding mechanism for late bound member functions of ITest.
The object 300 includes an instance data structure 302, a virtual function table 310, and method implementations 324–327. The instance data structure includes a pointer 304 to the virtual function table 310 and data 308 (e.g., properties). The virtual function table 310 includes a pointer for each member function of ITest that is inherited from the interface IDispatch. Again, the pointers 311–313 correspond to the standard COM IUnknown methods QueryInterface, AddReference, and Release, and point to implementations not shown in FIG. 3. The pointers 314–317 correspond to the remaining IDispatch methods, and point to implementations 324–327, respectively.
By calling the method GetIDsOfNames, a client retrieves a dispatch identifier [“DISPID”] for a particular name of a late bound member function. A DISPID is a type of token that is passed to the dispatch interface to invoke a corresponding member function. In FIG. 3, a client could retrieve the DISPID 18 by querying GetIDsOfNames for the DISPID of the method Score.
A client calls Invoke, passing a DISPID for a late bound method, arguments packed into a generic data structure, and other information. The Invoke implementation 327 maps DISPIDs to the implementations 224–226 for the other, non-dispatch methods of ITest. The Invoke implementation 327 calls the appropriate method for the passed DISPID. If a client passed the DISPID 18 to Invoke, the Invoke implementation 327 would call the method Score. For additional information about the dispatch interface, see Kraig Brockschmidt, Inside OLE, second edition, Chapters 14 and 15, Microsoft Press (1995).
Late binding interfaces provide flexibility and power, but not without a price. Manually programming the extra layers of code for late binding interfaces has proven to be notoriously difficult and time-consuming for programmers. Writing the corresponding client side code for packing arguments to and unpacking arguments from the generic data structures has also proven difficult.
While several attempts have been made to simplify the programming of late binding interfaces, none has been completely successful. Typically, these attempts simplify the act of creating dispatch interface implementations, but add unnecessary run time overhead to the resulting implementations. In comparison, manually created dispatch interface implementations, though difficult to create, are relatively efficient when executing.
Microsoft Corporation provides type library support for dispatch interfaces. A type library object includes definition information about an interface, and can expose methods GetIDsOfNames and Invoke for the interface. To implement a dispatch interface in an object, a programmer can make use of these methods provided by the type library object. Although this hides the details of implementing the dispatch interface from the programmer, the resulting implementation requires numerous run time calls between the object and the type library object, which slows processing. Moreover, a type library implementation typically uses an early binding mechanism of a dual interface (an interface that has both early binding and late binding mechanisms). Essentially, the type library is used as a virtual function table, which is no more robust than a virtual function table for a custom interface—type library implementations are not very useful for distributed computing (e.g., with objects designed according to Microsoft Corporation's Distributed Component Object Model [“DCOM”]), where the server may be several versions newer than the client, and hence type library information may be out of date. For additional information about implementing dispatch interfaces with a type library, see Shepherd et al, MFC Internals, Chapter 14, “MFC and Automation,” Addison Wesley (1996).
Microsoft Foundation Classes [“MFC”] also provides some support for implementing dispatch interfaces. MFC provides a high-level software tool called a wizard that simplifies the act of creating dispatch interface implementations. The wizard manipulates the source code for an object and helps create a dispatch map, which is a table that associates names with DISPIDs. At run time, the dispatch map is queried in multiple calls between the object and a special dispatch object. Getting DISPIDs through this mechanism is slow, as is invoking late bound methods. Further, having a wizard implement the dispatch interface can lead to source code that is confusing and unstable when changed by a programmer. If the definition for a member function changes, however, the programmer may need to change the confusing and unstable wizard-generated code. Finally, while MFC provides a dispatch implementation tool for MFC, the tool does not work for other platforms. For additional information about implementing dispatch interfaces with MFC, see Shepherd et al, MFC Internals, Chapter 14, “MFC and Automation,” Addison Wesley (1996).